在前一篇文章中,我們優化了 Service Section,並展示了如何使用 CSS Grid Layout 來構建響應式卡片佈局。今天,我們將進一步進階,自己實作一個類似 Swiper.js 的滑動元件,並根據需求自定義屬性。這個元件將允許我們靈活地顯示項目、設定自動播放、無限循環等效果,並且不依賴於第三方庫。
在這篇文章中,我們實作了類似於 Swiper.js 的滑動元件,以下是 Swiper 元件的屬性表,這些屬性能夠靈活地控制每個滑動項目的行為和外觀。
屬性 | 說明 | 類型 | 預設值 |
---|---|---|---|
items |
要顯示的資料陣列,每個項目都包含 id 、title 、description 和 image 等屬性 |
array |
[] |
loop |
控制是否無限循環滑動 | boolean |
false |
autoplay |
控制是否自動播放滑動 | boolean |
false |
delay |
自動播放的延遲時間(以毫秒為單位) | number |
4000 |
direction |
滑動的方向,可選 horizontal 或 vertical |
string |
horizontal |
onSlideChange |
當滑動發生時觸發的回調函數,回傳當前滑動項的索引 | function |
null |
我們將首先定義一個 Swiper 元件,並將滑動項目資料作為 props
傳入。這個元件將會使用 CSS 來控制滑動效果。
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import PropTypes from 'prop-types';
import * as styles from './Swiper.module.scss';
const Swiper = ({
items,
loop = false,
autoplay = false,
delay = 3000,
direction = 'horizontal',
onSlideChange,
}) => {
const [currentIndex, setCurrentIndex] = useState(0);
const totalItems = items.length;
useEffect(() => {
let interval;
if (autoplay) {
interval = setInterval(() => {
nextSlide();
}, delay);
}
return () => clearInterval(interval);
}, [currentIndex, autoplay, delay]);
const nextSlide = () => {
if (currentIndex < totalItems - 1) {
setCurrentIndex(currentIndex + 1);
} else if (loop) {
setCurrentIndex(0);
}
if (onSlideChange) onSlideChange(currentIndex + 1);
};
const prevSlide = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
} else if (loop) {
setCurrentIndex(totalItems - 1);
}
if (onSlideChange) onSlideChange(currentIndex - 1);
};
return (
<div className={`${styles.swiperContainer} ${direction === 'vertical' ? styles.vertical : ''}`}>
<AnimatePresence>
<motion.div
key={items[currentIndex].id}
className={styles.swiperSlide}
initial={{ opacity: 0, x: direction === 'horizontal' ? 100 : 0, y: direction === 'vertical' ? 100 : 0 }}
animate={{ opacity: 1, x: 0, y: 0 }}
exit={{ opacity: 0, x: direction === 'horizontal' ? -100 : 0, y: direction === 'vertical' ? -100 : 0 }}
transition={{ duration: 0.5 }}
>
<img src={require(`@/assets/img/${items[currentIndex].image}`)} alt={items[currentIndex].title} className={styles.swiperImage} />
<div className={styles.swiperContent}>
<h2>{items[currentIndex].title}</h2>
<p>{items[currentIndex].description}</p>
<div className={styles.hashtags}>
{items[currentIndex].stack.map((tech, index) => (
<span key={index}>{tech}</span>
))}
</div>
<div className={styles.pagination}>
<button onClick={prevSlide} disabled={!loop && currentIndex === 0}>← Prev</button>
<span>{currentIndex + 1} / {totalItems}</span>
<button onClick={nextSlide} disabled={!loop && currentIndex === totalItems - 1}>Next →</button>
</div>
</div>
</motion.div>
</AnimatePresence>
</div>
);
};
Swiper.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
stack: PropTypes.arrayOf(PropTypes.string).isRequired,
})
).isRequired,
loop: PropTypes.bool,
autoplay: PropTypes.bool,
delay: PropTypes.number,
direction: PropTypes.oneOf(['horizontal', 'vertical']),
onSlideChange: PropTypes.func,
};
export default Swiper;
說明:
items
:滑動的內容,傳入每個專案的 id
、title
、description
、image
和 stack
。loop
:是否啟用無限循環(預設為 false
)。autoplay
:是否自動播放(預設為 false
)。delay
:自動播放的延遲時間(預設為 3000
毫秒)。direction
:滑動方向,可以是 horizontal
或 vertical
。onSlideChange
:滑動時觸發的回調函數,會傳回當前的 currentIndex
。我們的 Swiper 元件需要根據 direction
的不同來實現不同方向的滑動效果。此外,若 autoplay
被啟用,我們需要確保滑動的過渡效果是流暢的。
@import '@/styles/variables';
.swiperContainer {
position: relative;
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
.swiperSlide {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
transition: all 0.5s ease-in-out;
}
.swiperImage {
width: 100%;
max-width: 600px;
height: auto;
border-radius: 8px;
object-fit: cover;
}
.swiperContent {
margin-top: 20px;
}
.vertical .swiperSlide {
flex-direction: column;
height: 100%;
}
.pagination {
display: flex;
justify-content: space-between;
width: 100%;
margin-top: 20px;
}
說明:
vertical
樣式:當滑動方向為 vertical
時,確保滑動內容以垂直方式顯示。transition
:滑動卡片的過渡效果設定為 0.5 秒,讓過渡更加流暢。為了提升用戶的操作體驗,我們可以添加滑動手勢支援和鍵盤導航功能,讓 Swiper 在行動裝置和桌機上都能輕鬆操作。
const [touchStart, setTouchStart] = useState(null);
const handleTouchStart = (e) => {
const touchStartX = e.touches[0].clientX;
setTouchStart(touchStartX);
};
const handleTouchEnd = (e) => {
const touchEndX = e.changedTouches[0].clientX;
if (touchStart - touchEndX > 50) {
nextSlide();
} else if (touchStart - touchEndX < -50) {
prevSlide();
}
};
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight') {
nextSlide();
} else if (e.key === 'ArrowLeft') {
prevSlide();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
說明:
touchStart
和 touchEnd
監控滑動手勢,當滑動距離超過 50px 時,觸發下一頁或上一頁。在這篇文章中,我們深入探討了如何從零開始建立一個滑動元件(Swiper),並展示如何通過自定義的 props 靈活控制其行為。這樣的元件讓我們能夠避免使用第三方庫,在不同的專案中保持靈活性,同時適應各種 UI 和功能需求。
在實務應用中,這種元件設計模式特別適合需要動態展示內容的頁面,讓用戶有更佳的瀏覽體驗。如果你對於更多的前端挑戰或元件設計有興趣,請繼續關注接下來的文章!
✨ 流光館Luma<∕> ✨ 期待與你繼續探索更多技術知識!